package com.environment;

import java.text.DecimalFormat;
import java.util.ArrayList;

import com.environment.component.AstronomyComponent;
import com.environment.component.EffectComponent;
import com.environment.component.SkyComponent;
import com.environment.component.WeatherComponent;
import com.environment.component.enumn.EWeather;
import com.environment.unit.Component;
import com.environment.unit.IComponent;
import com.jme.math.Vector3f;
import com.jme.renderer.Renderer;
import com.jme.renderer.pass.BasicPassManager;
import com.jme.scene.Node;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.FogState;
import com.jme.scene.state.LightState;
import com.jme.scene.state.ZBufferState;
import com.jme.system.DisplaySystem;
import com.jmex.effects.glsl.BloomRenderPass;
import com.jmex.effects.particles.ParticleMesh;

/**
 * <code>Environment</code> extends <code>Component</code> and maintains several
 * other <code>Component</code> to represent a complete environment system.
 * <p>
 * All the <code>Component</code> should be connected to other essential
 * <code>Component</code> before connecting to <code>Environment</code>.
 * <p>
 * When <code>Environment</code> connects to a <code>Component</code> that
 * <code>Component</code> is automatically initialized and connected back to 
 * <code>Environment</code>.
 * <p>
 * <code>Environment</code> also maintains all the essential <code>RenderState</code>
 * for all the <code>Component</code> which connects to <code>Environment</code> to use.
 * 
 * @author Yi Wang (Neakor)
 * @version Creation date: 04-11-2008 16:52 EST
 * @version Modified date: 06-15-2008 20:20 EST
 */
public class Environment extends Component{
	/**
	 * Serial version.
	 */
	private static final long serialVersionUID = -8545986315459104199L;
	/**
	 * The <code>Renderer</code> object.
	 */
	private final Renderer renderer;
	/**
	 * The <code>ArrayList</code> of <code>Component</code> that are connected
	 * to this <code>Environment</code>.
	 */
	private final ArrayList<Component> components;
	/**
	 * The <code>BlendState</code> for transparency.
	 */
	private BlendState blend;
	/**
	 * The quick reference to <code>SkyComponent</code>.
	 */
	private SkyComponent sky;
	/**
	 * The quick reference to <code>AstronomyComponent</code>.
	 */
	private AstronomyComponent astronomy;
	/**
	 * The quick reference to <code>EffectComponent</code>.
	 */
	private EffectComponent effect;
	/**
	 * The quick reference to <code>WeatherComponent</code>.
	 */
	private WeatherComponent weather;
	/**
	 * The temporary current time float for update.
	 */
	private float currentTime;
	/**
	 * The temporary <code>Vector3f</code> camera location.
	 */
	private final Vector3f tempLocation;
	
	/**
	 * Constructor of <code>Environment</code>.
	 */
	public Environment() {
		super(Environment.class.toString());
		this.renderer = DisplaySystem.getDisplaySystem().getRenderer();
		this.components = new ArrayList<Component>();
		this.tempLocation = new Vector3f();
		this.buildBlendState();
		this.buildZBufferState();
		this.setModes();
	}

	@Override
	public boolean validate() {
		return true;
	}

	@Override
	protected void initializeDetail() {};

	@Override
	protected void updateDetail(float interpolation) {
		this.tempLocation.set(this.renderer.getCamera().getLocation());
		this.setLocalTranslation(this.tempLocation.x, this.tempLocation.y-0.1f*this.sky.getSkyRadius(), this.tempLocation.z);
		this.currentTime = this.getTime(false);
		for(Component c: this.components) {
			c.update(this.currentTime);
		}
	}
	
	/**
	 * Build the <code>BlendState</code>.
	 */
	private void buildBlendState() {
		this.blend = this.renderer.createBlendState();
		this.blend.setBlendEnabled(true);
		this.blend.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
		this.blend.setDestinationFunction(BlendState.DestinationFunction.OneMinusSourceAlpha);
		this.blend.setTestEnabled(true);
		this.blend.setTestFunction(BlendState.TestFunction.GreaterThan);
		this.blend.setEnabled(true);
	}
	
	/**
	 * Set up the <code>ZBufferState</code>.
	 */
	private void buildZBufferState() {
	    ZBufferState zbuff = this.renderer.createZBufferState();
	    // XXX Enabled writable makes it possible to cull out moon and sun. However, better texture is needed first.
	    zbuff.setWritable(false);
	    zbuff.setEnabled(true);
	    zbuff.setFunction(ZBufferState.TestFunction.LessThanOrEqualTo);
	    this.setRenderState(zbuff);
	    this.updateRenderState();
	}
	
	/**
	 * Set the texture combine mode, render queue mode, and cull mode.
	 */
	private void setModes() {
		this.setTextureCombineMode(TextureCombineMode.Replace);
		this.setRenderQueueMode(Renderer.QUEUE_SKIP);
		this.setCullHint(CullHint.Never);
	}
	
	@Override
	public void connect(IComponent component) {
		component.connect(this);
		component.initialize();
		this.components.add((Component)component);
		if(component instanceof SkyComponent) {
			this.sky = (SkyComponent)component;
			this.attachChild(this.sky);
		} else if(component instanceof AstronomyComponent) {
			this.astronomy = (AstronomyComponent)component;
			this.attachChild(this.astronomy);
		} else if(component instanceof EffectComponent) {
			this.effect = (EffectComponent)component;
		} else if(component instanceof WeatherComponent) {
			this.weather = (WeatherComponent)component;
		}
	}
	
	/**
	 * Apply everything that does not require <code>RenderPass</code> provided by
	 * <code>Environment</code> to the given scene.
	 * @param rootNode The root <code>Node</code> of the scene.
	 */
	public void applyToScene(Node rootNode) {
		this.applyToScene(rootNode, null);
	}
	
	/**
	 * Apply everything provided by <code>Environment</code> to the given scene.
	 * @param rootNode The root <code>Node</code> of the scene.
	 * @param pManager The <code>BasicPassManager</code> of the scene.
	 */
	public void applyToScene(Node rootNode, BasicPassManager pManager) {
		rootNode.attachChild(this);
		rootNode.setRenderState(this.getFogState());
		rootNode.setRenderState(this.getLighting());
		if(pManager != null) pManager.add(this.getBlooming());
		rootNode.updateRenderState();
	}

	/**
	 * Set the <code>Environment</code> time with the given String.
	 * @param currentTime The String representation of time in form of HH.mm.
	 */
	public void setCurrentTime(String currentTime) {
		float animationTime = this.toAnimationTime(currentTime);
		this.astronomy.getAnimatior().setCurTime(animationTime);
	}
	
	/**
	 * Convert the given String representation of time into the animation time system.
	 * @param currentTime The String representation of time in the form of <HH.mm>.
	 * @return The converted time in animation time system.
	 */
	private float toAnimationTime(String currentTime) {
		// Calculate the total minutes elapsed.
		int hour = Integer.valueOf(currentTime.substring(0, currentTime.indexOf(".")));
		int minute = Integer.valueOf(currentTime.substring(currentTime.indexOf(".")+1, currentTime.length()));
		float totalMinute = hour * 60 + minute;
		// Convert the total minute into animation time system.
		float animationTime = (totalMinute/(24*60))*this.astronomy.getLengthOfDay();
		// Return the animation time.
		return animationTime;
	}
	
	/**
	 * Set the weather effect with given <code>Weather</code> type to the given Node.
	 * @param weather The type of <code>Weather</code> needs to be set.
	 * @param root The root <code>Node</code> of the scene graph.
	 */
	public void setWeather(EWeather weather, Node root) {
		ParticleMesh particle = this.weather.getWeather(weather);
		if(particle != null) {
			particle.warmUp(150);
			root.attachChild(particle);
		} else {
			ParticleMesh rain = this.weather.getWeather(EWeather.Rain);
			ParticleMesh snow = this.weather.getWeather(EWeather.Snow);
			root.detachChild(rain);
			root.detachChild(snow);
		}
		root.updateRenderState();
	}

	/**
	 * Retrieve the current time of the EnvironmentSystem in 24 hour time system.
	 * Typically the minutes should not be converted if the returned value is used
	 * to set a scale value.
	 * @param convertMinutes True if the returned time is in 60 minute system. False if the returned time is in 100 minute system.
	 * @return The current time of the EnvironmentSystem in 24 hour time system.
	 */
	public float getTime(boolean convertMinutes) {
		// Get the raw time.
		String rawTimeStr = String.valueOf(this.astronomy.getAnimatior().getCurTime()/(this.astronomy.getLengthOfDay()/24));
		// If argument is true, convert the minutes as well.
		if(convertMinutes) {
			// Convert the number before the decimal into 24 hour system.
			String hourStr = rawTimeStr.substring(0, rawTimeStr.lastIndexOf("."));
			int hour = Integer.valueOf(hourStr);
			// Convert the number after the decimal into 60 minute system.
			DecimalFormat minuteFormatter = new DecimalFormat("#.##");
			String rawMinuteStr = "0" + rawTimeStr.substring(rawTimeStr.lastIndexOf("."), rawTimeStr.length());
			String minuteStr = minuteFormatter.format(60*Float.valueOf(rawMinuteStr)/100);
			float minute = Float.valueOf(minuteStr);
			if(minute == 0.6f) {
				minute = 1;
			}
			// Restore and return the complete earth time.
			float rawEarthTime = hour + minute;
			if(rawEarthTime == 24) {
				rawEarthTime = 0.0f;
			}
			DecimalFormat timeFormatter = new DecimalFormat("##.##");
			String earthTimeStr = timeFormatter.format(rawEarthTime);
			return Float.valueOf(earthTimeStr);
		// Otherwise, return raw time.
		} else {
			return Float.valueOf(rawTimeStr);
		}
	}
	
	/**
	 * Retrieve the <code>Renderer</code> object.
	 * @return The <code>Renderer</code> object.
	 */
	public Renderer getRenderer() {
		return this.renderer;
	}
	
	/**
	 * Retrieve the <code>BlendState</code> object.
	 * @return The <code>BlendState</code> object.
	 */
	public BlendState getAlphaState() {
		return this.blend;
	}
	
	/**
	 * Retrieve the astronomy lighting <code>LightState</code>.
	 * @return The astronomy lighting <code>LightState</code>.
	 */
	public LightState getLighting() {
		return this.astronomy.getAstronomyLight();
	}
	
	/**
	 * Retrieve the per-vertex <code>FogState</code>.
	 * @return The per-vertex <code>FogState</code>.
	 */
	public FogState getFogState() {
		return this.effect.getFogState();
	}
	
	/**
	 * Retrieve the <code>BloomRenderPass</code> object.
	 * @return The <code>BloomRenderPass</code> object.
	 */
	public BloomRenderPass getBlooming() {
		return this.effect.getBlooming();
	}
}
